package se.kayarr.cardplay.gameplay;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import lombok.Getter;

import org.pircbotx.PircBotX;

import se.kayarr.cardplay.exceptions.CardNotInHandException;
import se.kayarr.cardplay.exceptions.FieldPositionOccupiedException;
import se.kayarr.cardplay.exceptions.NoTargetException;
import se.kayarr.cardplay.exceptions.PositionOutsideFieldException;
import se.kayarr.cardplay.exceptions.TooManyPlayersException;
import se.kayarr.cardplay.exceptions.UnitsOnFieldException;
import se.kayarr.cardplay.gameplay.UnitCard.Ability;
import se.kayarr.cardplay.gameplay.UnitCard.Instance;
import se.kayarr.cardplay.shared.ClampedValue;
import se.kayarr.cardplay.shared.StringReferenceParser;
import se.kayarr.cardplay.shared.StringReferenceParser.NoSuchPlayerException;
import se.kayarr.cardplay.shared.Util;

public class Player implements Targetable {
	@Getter private Session session;
	@Getter private Field field;
	
	@Getter private String ircUser;
	private PircBotX bot;
	
	@Getter private ClampedValue health;
	
	@Getter private List<Card.Instance> hand;
	@Getter private List<Card.Instance> deck;
	
	@Getter private boolean alive = false;
	
	/*
	public List<Card.Instance> getHand() {
		return Collections.unmodifiableList(hand);
	}
	//*/
	
	private Set<UnitCard.Instance> usedUnits = new HashSet<>();
	
	void clearUsedUnits() {
		usedUnits.clear();
	}
	
	public Player(Session session, PircBotX bot, String ircUser) {
		this.session = session;
		this.bot = bot;
		this.ircUser = ircUser;
		
		this.field = new Field(this, 6, 2);
		
		health = new ClampedValue(500);
		health.setToMax();
	}
	
	void prepare() {
		deck = CardManager.getInstance().generateDeck(session, this);
		hand = new ArrayList<>();
		
		for(int i = 0; i < 5; i++) {
			hand.add(deck.remove(deck.size()-1));
		}
		
		alive = true;
	}
	
	private void placeUnitCard(int x, int y, UnitCard.Instance instance)
			throws FieldPositionOccupiedException, CardNotInHandException, PositionOutsideFieldException {
		
		if(!hand.contains(instance)) throw new CardNotInHandException(this, instance);
		
		if(field.canPlaceUnitCard(x, y, instance))
			session.narrate(getName() + " placed the card " + instance + " at " + (x+1) + "," + (y+1) + "!");
		
		field.putUnitCard(x, y, instance);
		
		hand.remove(instance); //TODO Move to discard pile
		usedUnits.add(instance);
	}
	
	private void useUnitAbility(UnitCard.Instance instance, int abilityNum, String refString)
			throws NoTargetException, UnitsOnFieldException, TooManyPlayersException {
		
		if(usedUnits.contains(instance)) {
			notice("You can't use this Unit! You either already used it, or you just placed it!");
			return;
		}
		
		List<UnitCard.Ability> abilities = instance.getAbilities();
		
		if(abilityNum < 0 || abilityNum >= abilities.size()) throw new IndexOutOfBoundsException();
		
		Ability ability = abilities.get(abilityNum);
		
		Targetable target;
		try {
			target = refString != null ? session.getTargetByString(this, refString, ability.getTargetFlags()) : null;
		}
		catch (NoSuchPlayerException e) {
			target = null;
		}
		
		if(ability.getTargetFlags() != Targetable.NONE && target == null) throw new NoTargetException(ability.getTargetFlags());
		if(target instanceof Player && ((Player)target).field.unitsOnField() > 0) throw new UnitsOnFieldException();
		
		String targetString = "";
		if(target != null) targetString = target.toString();
		if(target instanceof UnitCard.Instance) {
			UnitCard.Instance unitTarget = (Instance) target;
			targetString = unitTarget.getOwner().getName() + "'s " + unitTarget;
		}
		
		getSession().narrate(getName() + " uses ability #" + (abilityNum+1)
				+ ", " + ability.getName() + ", of " + instance +
				(target != null ? " on " + targetString : "") + "!");
		
		if(ability.onUse(session, this, instance, target)) usedUnits.add(instance);
		
		session.cleanupDead();
	}
	
	private void useActionCard(ActionCard.Instance instance, String refString)
			throws NoTargetException, UnitsOnFieldException, TooManyPlayersException, CardNotInHandException {
		
		if(!hand.contains(instance)) throw new CardNotInHandException(this, instance);
		
		int targetFlags = instance.getCard().getTargetFlags();
		
		Targetable target;
		try {
			target = refString != null ? session.getTargetByString(this, refString, targetFlags) : null;
		}
		catch (NoSuchPlayerException e) {
			target = null;
		}
		
		if(targetFlags != Targetable.NONE && target == null) throw new NoTargetException(targetFlags);
		if(target instanceof Player && ((Player)target).field.unitsOnField() > 0) throw new UnitsOnFieldException();
		
		String targetString = "";
		if(target != null) targetString = target.toString();
		if(target instanceof UnitCard.Instance) {
			UnitCard.Instance unitTarget = (Instance) target;
			targetString = unitTarget.getOwner().getName() + "'s " + unitTarget;
		}
		
		getSession().narrate(getName() + " uses Action Card " + instance + (target != null ? " on " + targetString : "") + "!");
		
		instance.onUse(this, target);
		
		hand.remove(instance); //TODO Move to discard pile maybe
		
		session.cleanupDead();
	}
	
	public int dealDamage(int damage) {
		return dealDamage(damage, Type.NEUTRAL);
	}
	
	public int dealDamage(int damage, Type damageType) {
		damage *= Type.getDamageMultiplier(damageType, Type.NEUTRAL); //A player's type is always assumed to be Neutral
		health.removeAmount(damage);
		return damage;
	}
	
	public String toString() {
		return "Player " + getName() + " " + health + " (" + hand.size() + " cards on hand, " + deck.size() + " cards in deck)";
	}

	@Override
	public String getName() {
		return ircUser;
	}
	
	void handleCommandUse(String command) {
		System.out.println(ircUser + " in session " + getSession().getIdentifier() + " used command \"" + command + "\"");
		
		String[] commandParts = command.split("\\s+", 2);
		switch(commandParts[0].toLowerCase()) {
			case "debug-neardead": case "dnd": {
				health.setCurrent(health.getMin()+1);
				break;
			}
			
			case "debug-getcards": case "dgc": {
				if(getSession().getCurrentPlayer() != this) {
					notice("It is not currently your turn!");
					break;
				}
				
				hand.clear();
				for(int i = 0; i < 7; i++) {
					List<Card> cards = CardManager.getInstance().getCards();
					Card card = cards.get( Util.getRandom().nextInt(cards.size()) );
					Card.Instance instance = card.newInstance(getSession(), this);
					hand.add(instance);
				}
				showCards();
				break;
			}
				
			case "hand": case "cards": //Format: hand|cards
				showCards();
				break;
				
			case "draw": {
				if(getSession().getCurrentPlayer() != this) {
					notice("It is not currently your turn!");
					break;
				}
				
				if(deck.size() == 0) {
					notice("Your deck is empty, can't draw a card!");
					break;
				}
				
				Card.Instance instance = deck.remove(deck.size()-1);
				notice("You got: " + instance);
				hand.add(instance);
				
				break;
			}
				
			case "place": { //Format: place [unit in hand] [x] [y]
				if(getSession().getCurrentPlayer() != this) {
					notice("It is not currently your turn!");
					break;
				}
				
				if(commandParts.length > 1) {
					String[] subParts = commandParts[1].split("\\s+");
					if(subParts.length >= 3) {
						try {
							Card.Instance instance =
									StringReferenceParser.getInstanceFrom(subParts[0], getHand());
							
							if(instance == null) {
								notice("You do not have a card like that on your hand!");
								return;
							}
							
							if( !(instance instanceof UnitCard.Instance) ) {
								notice("That is not a Unit card!");
								return;
							}
							
							UnitCard.Instance unit = (Instance) instance;
							
							int x = Integer.parseInt(subParts[1]) - 1;
							int y = Integer.parseInt(subParts[2]) - 1;
							
							placeUnitCard(x, y, unit);
						}
						catch(NumberFormatException e) {
							notice("I was unable to parse that \"number\": " + e.getMessage());
						}
						catch(FieldPositionOccupiedException e) {
							notice("That position is already taken!");
						}
						catch(PositionOutsideFieldException e) {
							notice("You can't place a Unit outside the field!");
						}
						catch (CardNotInHandException e) {} //Handled already, when checking if instance == null, never thrown
					}
				}
				
				break;
			}
			
			case "ability": { //Format: ability [unit on field] [ability number] {target}
				if(getSession().getCurrentPlayer() != this) {
					notice("It is not currently your turn!");
					break;
				}
				
				if(commandParts.length > 1) {
					String[] abilityParts = commandParts[1].split("\\s+");
					
					if(abilityParts.length >= 2) {
						try {
							UnitCard.Instance instance = StringReferenceParser.getInstanceFrom(abilityParts[0], getField());
							
							if(instance == null) {
								notice("Unable to find a valid card!");
								return;
							}
							
							int abilityNum = Integer.parseInt(abilityParts[1]) - 1;
							
							useUnitAbility(instance, abilityNum, abilityParts.length >= 3 ? abilityParts[2] : null);
						}
						catch(IndexOutOfBoundsException | NumberFormatException e) {
							notice("That's not a valid ability!");
						}
						catch (NoTargetException e) {
							notice("You need to specify a valid target for this ability! Allowed targets: " +
									Util.getPrintableTargetFlags(e.getTargetFlags()));
						}
						catch (UnitsOnFieldException e) {
							notice("You can't target an opponent player while " +
									"they still have units on their field!");
						}
						catch (TooManyPlayersException e) {
							notice("You can't use @ when there are more than two players!");
						}
						catch (PositionOutsideFieldException e) {
							notice("The position " + e.getX() + ", " + e.getY() + " is outside of the specified field!");
						}
					}
				}
				
				break;
			}
				
			case "use": {
				if(getSession().getCurrentPlayer() != this) {
					notice("It is not currently your turn!");
					break;
				}
				
				if(commandParts.length > 1) {
					String[] actionParts = commandParts[1].split("\\s+");
					
					try {
						Card.Instance instance = StringReferenceParser.getInstanceFrom(actionParts[0], getHand());
						
						if(instance == null) {
							notice("You do not have a card like that on your hand!");
							return;
						}
						
						if( !(instance instanceof ActionCard.Instance) ) {
							notice("That's not an Action Card!");
							return;
						}
						
						ActionCard.Instance action = (ActionCard.Instance) instance;
						
						useActionCard(action, actionParts.length >= 2 ? actionParts[1] : null);
					}
					catch (NoTargetException e) {
						notice("You need to specify a valid target for this Action! Allowed targets: " +
								Util.getPrintableTargetFlags(e.getTargetFlags()));
					}
					catch (UnitsOnFieldException e) {
						notice("You can't target an opponent player while " +
								"they still have units on their field!");
					}
					catch (TooManyPlayersException e) {
						notice("You can't use @ when there are more than two players!");
					}
					catch (CardNotInHandException e) {} //Handled already, when checking if instance == null, never thrown
				}
				
				break;
			}
			
			case "debug-findcard": case "dfc": {
				if(commandParts.length > 1) {
					try {
						Card.Instance instance = StringReferenceParser.getReferencedInstance(commandParts[1], getSession(), this);
						
						if(instance != null) getSession().narrate("Found card: " + instance);
						else getSession().narrate("Can't find that card!");
					}
					catch(Exception e) {
						notice("EXCEPTION: " + e);
					}
				}
				break;
			}
				
			case "pass": {//Format: pass
				if(getSession().getCurrentPlayer() != this) {
					notice("It is not currently your turn!");
					break;
				}
				
				getSession().narrate(ircUser + " passes their turn!");
				getSession().progressTurn();
				break;
			}
				
			default:
				notice("Unknown command: " + commandParts[0]);
				break;
		}
	}
	
	public void notice(String message) {
		System.out.println(getName() + " @ " + session.getIdentifier() +  " NOTICE: " + message);
		bot.sendNotice(ircUser, message);
	}
	
	void showCards() {
		if(getHand().size() > 0) notice("You got "+hand.size()+" cards on hand: " + Util.join(", ", getHand()));
		else notice("You currently have no cards.");
	}
	
	void onDeath() {
		session.narrate(getName() + " has died...");
		
		hand.clear();
		deck.clear();
		field = null;
		
		alive = false;
	}
}
